As-sertion casting
So you’ve got some code that you know has to be in a certain state. It can’t be proven at compile-time, but you know that it has to be like this, and if it wasn’t, it could cause some serious problems. But you’re a good, careful coder, and you don’t want to just assume that the state of the program will be a certain way just because you think it should. You know better than that, so you add a bit of special code to verify your assumptions. And if it turns out you were wrong, it’ll raise an exception that you almost certainly have nothing in place to catch at any level before the global exception handler for your app.
Sound familiar? It should. You probably do it all the time. It’s an as-cast.
…what? You thought I was going to say “assertion”? Good point! An as-cast is a whole lot like an assertion. There’s one big difference, though. When an assertion fails, your error log ends up giving you some very useful (and easily customizable) information about the nature of the error. With an as-cast failure… nothing. Granted, any decent error-logging framework will give you the unit name and line number anyway, but assertions still offer custom error messages that can make maintenance easier. (And, if your app doesn’t have an error-logging framework installed, the assertion failure can point you directly to whatever’s going wrong faster than just about anything else.)
Now I’m not saying that as-casts are bad. They can be useful, just like assertions can. But I often see code that uses as-casts when they’re not needed, and it feels like cargo cult programming to me. For example, have you ever seen (or written) a construct like this?
[code lang="Delphi"] if sender is TMyObject then (sender as TMyObject).DoSomething; [/code]
What’s wrong with this picture? Let’s look at what an as-cast does. From system.pas:
[code lang="Delphi"] function _AsClass(Child: TObject; Parent: TClass): TObject; {$IFDEF PUREPASCAL} begin Result := Child; if not (Child is Parent) then Error(reInvalidCast); // loses return address end; [/code]
I’ll skip the assembly version, though it’s worth noting that the PUREPASCAL reference code isn’t completely accurate. (Think about how it would handle a nil object reference.) But it’s close enough. If we substitute the code into my example, we get this:
[code lang="Delphi"] if Sender is TMyObject then begin if not (Sender is TMyObject) then Error(reInvalidCast); // loses return address end; TMyObject(Sender).DoSomething; [/code]
Why are we calling is twice? Unless some other thread is replacing one object with another in between the two lines, there’s nothing to worry about, and if you do have threads behaving that way, you’ve got bigger issues to worry about. But even so, does it really matter? Would that be a bad thing to do? Well, with all due credit to Bill Clinton, that depends on what the definition of is is. Fortunately, it’s defined in system.pas, right above the definition of as. It simply checks for nil, then calls InheritsFrom on the object. So what does InheritsFrom do? Here’s the PUREPASCAL version:
[code lang="Delphi"] class function TObject.InheritsFrom(AClass: TClass): Boolean; {$IFDEF PUREPASCAL} var ClassPtr: TClass; begin ClassPtr := Self; while (ClassPtr <> nil) and (ClassPtr <> AClass) do ClassPtr := PPointer(Integer(ClassPtr) + vmtParent)^; Result := ClassPtr = AClass; end; [/code]
It’s basically doing a linear search of a linked list, which runs in O(n) time and isn’t particularly cache-friendly. Not much to worry about, though, unless you do it in a tight loop. But when you do, the results can be informative. One thing I didn’t mention in my post a few weeks ago about optimizing a routine at work was the impact of as-casts. I didn’t mention it because it became irrelevant once I removed the horrendously inefficient comparison double-loop, but when it was there, the profiler found that 15% of the time was spent in System._AsClass. Why? Because our ORM object list was doing an as-cast in its Get routine.
You’ve probably seen custom object lists before, wrappers around TObjectList that only hold certain kinds of objects. This was one of those, and on its Get routine it did something like this:
[code lang="Delphi"] function TOrmObjectList.Get(index: integer): TOrmObject; begin result := inherited Get(index) as TOrmObject; end; [/code]
Our inheritance tree was pretty deep, and so every time it tried to retrieve an object from the list, it had to walk a linked list 6 or 7 steps before arriving at the base class the as-cast was looking for. Do that a few million times and it starts to really add up. And the thing is, it wasn’t even needed there because the Put method would only accept TOrmObject instances anyway. The compiler’s type safety made the cast redundant, and I ended up replacing it with a hard cast. (And yes, there’s always the possibility of memory corruption. But in my experience, if a bad pointer ends up stomping your RAM and writing over your data, the likelihood that whatever essentially random bytes overwrite your object reference will also be a pointer to a valid object of a different class is extremely low, and an as-cast in this case is far more likely to produce an access violation than an EInvalidCast exception. This will become even more true when we go to 64 bits and the size of a pointer doubles.)
Again, as-casts aren’t bad. I’ve caught a fair number of errors with them, and I’d be the last to say they’re “considered harmful” or anything like that. But using them when there’s no need for one doesn’t actually improve safety and can result in significant performance hits if it’s used in a tight loop. A hard cast has zero performance impact at runtime. No code is generated for it; it’s just there to satisfy the compiler’s type safety rules. So when you write your typecasts, ask yourself if you really need an as-sertion here.
Interesting post, I always use hard cast, but I’m always sure that the variable is of that type, so I just shut up the compiler.
By the way this code
=============================================================
class function TObject.InheritsFrom(AClass: TClass): Boolean;
{$IFDEF PUREPASCAL}
var ClassPtr: TClass;
begin
ClassPtr := Self;
while (ClassPtr nil) and (ClassPtr AClass) do
ClassPtr := PPointer(Integer(ClassPtr) + vmtParent)^;
Result := ClassPtr = AClass; end;
=============================================================
will not compile, you’ve started a $IFDEF and did not close it with $ENDIF :).